package com.xiaoyushu.note.biz.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.xiaoyushu.framework.biz.context.holder.LoginUserContextHolder;
import com.xiaoyushu.framework.common.exception.BizException;
import com.xiaoyushu.framework.common.response.Response;
import com.xiaoyushu.framework.common.util.JsonUtils;
import com.xiaoyushu.note.biz.constants.MQConstants;
import com.xiaoyushu.note.biz.constants.RedisKeyConstants;
import com.xiaoyushu.note.biz.domain.dataobject.NoteCollectionDO;
import com.xiaoyushu.note.biz.domain.dataobject.NoteDO;
import com.xiaoyushu.note.biz.domain.dataobject.NoteLikeDO;
import com.xiaoyushu.note.biz.domain.mapper.NoteCollectionDOMapper;
import com.xiaoyushu.note.biz.domain.mapper.NoteDOMapper;
import com.xiaoyushu.note.biz.domain.mapper.NoteLikeDOMapper;
import com.xiaoyushu.note.biz.domain.mapper.TopicDOMapper;
import com.xiaoyushu.note.biz.enums.*;
import com.xiaoyushu.note.biz.model.dto.CollectUnCollectNoteMqDTO;
import com.xiaoyushu.note.biz.model.dto.LikeUnlikeNoteMqDTO;
import com.xiaoyushu.note.biz.model.dto.NoteOperateMqDTO;
import com.xiaoyushu.note.biz.model.vo.*;
import com.xiaoyushu.note.biz.rpc.DistributedIdGeneratorRpcService;
import com.xiaoyushu.note.biz.rpc.KeyValueRpcService;
import com.xiaoyushu.note.biz.rpc.UserRpcService;
import com.xiaoyushu.note.biz.service.NoteService;
import com.xiaoyushu.user.dto.resp.FindUserByIdRspDTO;
import com.xiaoyushu.user.relation.biz.util.DateUtils;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class NoteServiceImpl implements NoteService {

    @Resource
    private NoteDOMapper noteDOMapper;
    @Resource
    private TopicDOMapper topicDOMapper;
    @Resource
    private DistributedIdGeneratorRpcService distributedIdGeneratorRpcService;
    @Resource
    private KeyValueRpcService keyValueRpcService;


    @Resource
    private UserRpcService userRpcService;

    @Resource(name = "taskExecutor")
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 笔记详情本地缓存
     */
    private static final Cache<Long, String> LOCAL_CACHE = Caffeine.newBuilder()
            .initialCapacity(10000) // 设置初始容量为 10000 个条目
            .maximumSize(10000) // 设置缓存的最大容量为 10000 个条目
            .expireAfterWrite(1, TimeUnit.HOURS) // 设置缓存条目在写入后 1 小时过期
            .build();
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    @Autowired
    private NoteLikeDOMapper noteLikeDOMapper;
    @Autowired
    private NoteCollectionDOMapper noteCollectionDOMapper;

    /**
     * 笔记发布
     *
     * @param publishNoteReqVO
     * @return
     */
    @Override
    public Response<?> publishNote(PublishNoteReqVo publishNoteReqVO) {
        // 笔记类型
        Integer type = publishNoteReqVO.getType();

        // 获取对应类型的枚举
        NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(type);

        // 若非图文、视频，抛出业务业务异常
        if (Objects.isNull(noteTypeEnum)) {
            throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);
        }

        String imgUris = null;
		// 笔记内容是否为空，默认值为 true，即空
        Boolean isContentEmpty = true;
        String videoUri = null;
        switch (noteTypeEnum) {
            case IMAGE_TEXT: // 图文笔记
                List<String> imgUriList = publishNoteReqVO.getImgUris();
                // 校验图片是否为空
                Preconditions.checkArgument(CollUtil.isNotEmpty(imgUriList), "笔记图片不能为空");
                // 校验图片数量
                Preconditions.checkArgument(imgUriList.size() <= 8, "笔记图片不能多于 8 张");
                // 将图片链接拼接，以逗号分隔
                imgUris = StringUtils.join(imgUriList, ",");

                break;
            case VIDEO: // 视频笔记
                videoUri = publishNoteReqVO.getVideoUri();
                // 校验视频链接是否为空
                Preconditions.checkArgument(StringUtils.isNotBlank(videoUri), "笔记视频不能为空");
                break;
            default:
                break;
        }
        
        // RPC: 调用分布式 ID 生成服务，生成笔记 ID
        String snowflakeIdId = distributedIdGeneratorRpcService.getSnowflakeId();
        // 笔记内容 UUID
        String contentUuid = null;

        // 笔记内容
        String content = publishNoteReqVO.getContent();

        // 若用户填写了笔记内容
        if (StringUtils.isNotBlank(content)) {
            // 内容是否为空，置为 false，即不为空
            isContentEmpty = false;
            // 生成笔记内容 UUID
            contentUuid = UUID.randomUUID().toString();
            // RPC: 调用 KV 键值服务，存储短文本
            boolean isSavedSuccess = keyValueRpcService.saveNoteContent(contentUuid, content);

            // 若存储失败，抛出业务异常，提示用户发布笔记失败
            if (!isSavedSuccess) {
                throw new BizException(ResponseCodeEnum.NOTE_PUBLISH_FAIL);
            }
        }

        // 话题
        Long topicId = publishNoteReqVO.getTopicId();
        String topicName = null;
        if (Objects.nonNull(topicId)) {
            // 获取话题名称
            topicName = topicDOMapper.selectNameByPrimaryKey(topicId);
        }

        // 发布者用户 ID
        Long creatorId = LoginUserContextHolder.getUserId();

        // 构建笔记 DO 对象
        NoteDO noteDO = NoteDO.builder()
                .id(Long.valueOf(snowflakeIdId))
                .isContentEmpty(isContentEmpty)
                .creatorId(creatorId)
                .imgUris(imgUris)
                .title(publishNoteReqVO.getTitle())
                .topicId(publishNoteReqVO.getTopicId())
                .topicName(topicName)
                .type(type)
                .visible(NoteVisibleEnum.PUBLIC.getCode())
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .status(NoteStatusEnum.NORMAL.getCode())
                .isTop(Boolean.FALSE)
                .videoUri(videoUri)
                .contentUuid(contentUuid)
                .build();

        try {
            // 笔记入库存储
            noteDOMapper.insert(noteDO);
        } catch (Exception e) {
            log.error("==> 笔记存储失败", e);
            
            // RPC: 笔记保存失败，则删除笔记内容
            if (StringUtils.isNotBlank(contentUuid)) {    
                keyValueRpcService.deleteNoteContent(contentUuid);
            }
        }
        //发送MQ
        //构建消息体 DTO
        NoteOperateMqDTO noteOperateMqDTO = NoteOperateMqDTO.builder()
                .creatorId(creatorId)
                .noteId(Long.valueOf(snowflakeIdId))
                .type(NoteOperateEnum.PUBLISH.getCode()) //发布笔记
                .build();

        //构建消息对象,并将DTO 转成Json 字符串设置到消息体中
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(noteOperateMqDTO))
                .build();
        //通过冒号连接,可让MQ发送 给主题Topic时,携带上标签Tag
        String destination = MQConstants.TOPIC_NOTE_OPERATE + ":" + MQConstants.TAG_NOTE_PUBLISH;

        //异步发送 MQ 消息,提升接口响应速度
        rocketMQTemplate.asyncSend(destination, message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 【笔记发布】MQ 发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 【笔记发布】MQ 发送异常: ", throwable);
            }
        });

        return Response.success();
    }

    public Response<FindNoteDetailRspVO> findNoteDetail1(FindNoteDetailReqVo findNoteDetailReqVO) {
        //查询的笔记ID
        Long noteId = findNoteDetailReqVO.getId();
        //当前登录用户
        Long userId = LoginUserContextHolder.getUserId();

        //从redis缓存红获取
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        String noteDetailJson = redisTemplate.opsForValue().get(noteDetailRedisKey);


        //从redis缓存中有该笔记的数据，则直接返回
        if (StringUtils.isNotBlank(noteDetailJson)){
            FindNoteDetailRspVO findNoteDetailRspVO= JsonUtils.parseObject(noteDetailJson, FindNoteDetailRspVO.class);
            //可见性校验
            if (Objects.nonNull(findNoteDetailRspVO)){
                Integer visible =findNoteDetailRspVO.getVisible();
                checkNoteVisible(visible,userId,findNoteDetailRspVO.getCreatorId());
            }
            return Response.success(findNoteDetailRspVO);
        }
        //若redis缓存中能干活去不到，则直接走数据库拆线呢
        //查询笔记
        NoteDO noteDO = noteDOMapper.selectByPrimaryKey(noteId);

        //若笔记不存在，则抛出业务异常
        if (Objects.isNull(noteDO)){
            threadPoolTaskExecutor.execute(()->{
                //防止缓存穿透，将空数据存入Redsi缓存中（过期时间不宜设置过长）
                //保底1分钟 + 随机秒数
                long expireSeconds = 60 + RandomUtil.randomInt(60);
                redisTemplate.opsForValue().set(noteDetailRedisKey, "null", expireSeconds, TimeUnit.SECONDS);
            });
            throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);
        }

        //可见性校验
        Integer visible = noteDO.getVisible();
        checkNoteVisible(visible,userId,noteDO.getCreatorId());
        // RPC：调用用户服务
      Long creatorId= noteDO.getCreatorId();
        FindUserByIdRspDTO findUserByIdRspDTO = userRpcService.findById(creatorId);
        //RPC 调用K-V 存储服务获取内容
        String content=null;
        if (Objects.equals(noteDO.getIsContentEmpty(),Boolean.FALSE)){
            content=keyValueRpcService.findNoteContent(noteDO.getContentUuid());
        }
        //笔记类型
        Integer noteType = noteDO.getType();
        //图片笔记图片连接（字符串）
        String imgUris = noteDO.getImgUris();
        //图片笔记图片连接（集合）
        List<String> imgUriList = null;
        //如果拆线呢的是图文笔记，需要将图片连接的逗号分隔开，装换成集合
        if (Objects.equals(noteType,NoteTypeEnum.IMAGE_TEXT.getCode())
        && StringUtils.isNotBlank(imgUris)){
            imgUriList=List.of(imgUris.split( ","));
        }
        //构建反参VO实体类
        FindNoteDetailRspVO build = FindNoteDetailRspVO.builder()
                .id(noteDO.getId())
                .type(noteDO.getType())
                .title(noteDO.getTitle())
                .content(content)
                .imgUris(imgUriList)
                .topicId(noteDO.getTopicId())
                .topicName(noteDO.getTopicName())
                .creatorId(noteDO.getCreatorId())
                .creatorName(findUserByIdRspDTO.getNickName())
                .avatar(findUserByIdRspDTO.getAvatar())
                .videoUri(noteDO.getVideoUri())
                .updateTime(noteDO.getUpdateTime())
                .visible(noteDO.getVisible())
                .build();
        //异步线程中将笔记详情存入Redis
        threadPoolTaskExecutor.submit(()->{
           String noteDetailJson1= JsonUtils.toJsonString( build);
            //过期时间（保底1天+随机秒数，将缓存过期时间打散，防止同一时间大量混村失效）
            long expireSeconds=60*60*24 + RandomUtil.randomInt(60*60*24);
            redisTemplate.opsForValue().set(noteDetailRedisKey, noteDetailJson1, expireSeconds, TimeUnit.SECONDS);
        });
        return Response.success(build);
    }

    public void checkNoteVisible(Integer visible,Long currUserId,Long creatorId){
        if (Objects.equals(visible,NoteVisibleEnum.PRIVATE.getCode())
        && !Objects.equals(currUserId,creatorId)){//仅自己可见，并且访问用户为笔记创建者才能访问，非本人则抛出异常
            throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);

        }
    }


    /**
     * 笔记详情
     * @param findNoteDetailReqVO
     * @return
     */
    @Override
    @SneakyThrows
    public Response<FindNoteDetailRspVO> findNoteDetail(FindNoteDetailReqVo findNoteDetailReqVO) {
        //查询的笔记ID
        Long noteId = findNoteDetailReqVO.getId();
        //当前登录用户
        Long userId = LoginUserContextHolder.getUserId();


        //从redis缓存红获取
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        String noteDetailJson = redisTemplate.opsForValue().get(noteDetailRedisKey);


        //从redis缓存中有该笔记的数据，则直接返回
        if (StringUtils.isNotBlank(noteDetailJson)){
            FindNoteDetailRspVO findNoteDetailRspVO= JsonUtils.parseObject(noteDetailJson, FindNoteDetailRspVO.class);
            //异步线程中将用户信息存入本地缓存
            threadPoolTaskExecutor.submit(() -> {
                //写入被电脑缓存
                LOCAL_CACHE.put(userId,
                        Objects.isNull(findNoteDetailRspVO)?"null":JsonUtils.toJsonString(findNoteDetailRspVO));
            });
            //可见性校验
            if (Objects.nonNull(findNoteDetailRspVO)){
                Integer visible =findNoteDetailRspVO.getVisible();
                checkNoteVisible(visible,userId,findNoteDetailRspVO.getCreatorId());
            }
            return Response.success(findNoteDetailRspVO);
        }
        //若redis缓存中能干活去不到，则直接走数据库拆线呢
        //查询笔记
        NoteDO noteDO = noteDOMapper.selectByPrimaryKey(noteId);

        //若笔记不存在，则抛出业务异常
        if (Objects.isNull(noteDO)){
            threadPoolTaskExecutor.execute(()->{
                //防止缓存穿透，将空数据存入Redsi缓存中（过期时间不宜设置过长）
                //保底1分钟 + 随机秒数
                long expireSeconds = 60 + RandomUtil.randomInt(60);
                redisTemplate.opsForValue().set(noteDetailRedisKey, "null", expireSeconds, TimeUnit.SECONDS);
            });
            throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);
        }
        //可见性校验
        Integer visible = noteDO.getVisible();
        checkNoteVisible(visible,userId,noteDO.getCreatorId());
        //并发查询优化
        //RPC:调用用户服务
        Long  creatorId=noteDO.getCreatorId();
        CompletableFuture<FindUserByIdRspDTO> userResultFuture=CompletableFuture
                .supplyAsync(()->userRpcService.findById(creatorId),threadPoolTaskExecutor);
        //RPC：调用K-V 存储服务获取内容
        CompletableFuture<String> contentResultFuture=CompletableFuture.completedFuture( null);
        if (Objects.equals(noteDO.getIsContentEmpty(), Boolean.FALSE)) {
            contentResultFuture=CompletableFuture
                    .supplyAsync(()->keyValueRpcService.findNoteContent(noteDO.getContentUuid()),threadPoolTaskExecutor);
        }
        CompletableFuture<String> finalContentResultFeture=contentResultFuture;
        CompletableFuture<FindNoteDetailRspVO> resultFuture =   CompletableFuture
                .allOf(userResultFuture,contentResultFuture)
                .thenApply(s->{
                    //获取Future 返回结果
                    FindUserByIdRspDTO findUserByIdRspDTO = userResultFuture.join();
                    String conten=finalContentResultFeture.join();
                    //笔记类型
                    Integer noteType=noteDO.getType();
                    //图文 笔记图片链接（字符串）
                    String imgUriStr=noteDO.getImgUris();

                    //图文笔记图片链接（集合）
                    List<String> imgUriList=null;
                    //如果查询的事图文笔记，需要将图片链接的逗号分隔开，转换成集合
                    if (Objects.equals(noteType,NoteTypeEnum.IMAGE_TEXT.getCode())
                    && StringUtils.isNotBlank(imgUriStr)){
                        imgUriList=List.of(imgUriStr.split( ","));

                    }
                    //构建反反参 vo 实体类
                    return FindNoteDetailRspVO.builder()
                            .id(noteDO.getId())
                            .type(noteDO.getType())
                            .title(noteDO.getTitle())
                            .content(conten)
                            .imgUris(imgUriList)
                            .topicId(noteDO.getTopicId())
                            .topicName(noteDO.getTopicName())
                            .creatorId(noteDO.getCreatorId())
                            .creatorName(findUserByIdRspDTO.getNickName())
                            .avatar(findUserByIdRspDTO.getAvatar())
                            .videoUri(noteDO.getVideoUri())
                            .updateTime(noteDO.getUpdateTime())
                            .visible(noteDO.getVisible())
                            .build();



                });


        //获取拼装后的FindNoteDetailRspVO
       FindNoteDetailRspVO findNoteDetailRspVO= resultFuture.get();
//异步线程中将笔记详情存入Redis缓存中
        threadPoolTaskExecutor.submit(()->{
           String noteDetailJson1= JsonUtils.toJsonString(findNoteDetailRspVO);
           //过期时间 （保底1天+随机秒数，将缓存过期时间打散，防止同一时间大量数据失效）
           long expireSeconds= 60*60*24 + RandomUtil.randomInt(60*60*24);
           redisTemplate.opsForValue().set(noteDetailRedisKey, noteDetailJson1, expireSeconds, TimeUnit.SECONDS);
        });

       return Response.success(findNoteDetailRspVO);

    }

    /**
     * 笔记更新
     *1.先判断图片数量不能超过8张
     * 2.判断图片是否为空
     * 3.判断视频是否为空
     * 4.笔记类型是否存在
     * 5.更新数据库
     * 6.删除redis缓存
     * 7删除本地缓存
     * @param updateNoteReqVO
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Response<?> updateNote(UpdateNoteReqVO updateNoteReqVO) {
        //笔记 ID
        Long noteId=updateNoteReqVO.getId();
        //笔记类型
        Integer type=updateNoteReqVO.getType();


        //获取对应类型的枚举
        NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(type);
        //若非图文、视频，抛出业务业务异常
        if (Objects.isNull(noteTypeEnum)){
            throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);
        }
        String imgUris = null;
        String videoUri = null;
        switch (noteTypeEnum){
            case IMAGE_TEXT://图文笔记
                List<String> imgUriList = updateNoteReqVO.getImgUris();
                //校验图片是否为空
                Preconditions.checkArgument(CollUtil.isNotEmpty(imgUriList), "笔记图片不能为空");
                //校验图片数量
                Preconditions.checkArgument(imgUriList.size() <= 8, "笔记图片不能多于 8 张");
                imgUris=StringUtils.join(imgUriList, ",");
                break;
            case VIDEO: //视频笔记
                videoUri = updateNoteReqVO.getVideoUri();
                //校验视频链接是否为空
                Preconditions.checkArgument(StringUtils.isNotBlank(videoUri), "笔记视频不能为空");
                break;
            default:
                    break;
        }
        //解决bug
        //当前登录用户ID
        Long currUserId= LoginUserContextHolder.getUserId();
        NoteDO selectNoteDO=noteDOMapper.selectByPrimaryKey(noteId);
        //笔记不存在
        if (Objects.isNull(selectNoteDO)){
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }
        //判断权限:非笔记发布者不允许更改笔记
        if (!Objects.equals(currUserId,selectNoteDO.getCreatorId())){
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }
        Long topicId =  updateNoteReqVO.getTopicId();
        String topicName = null;
        if (Objects.nonNull(topicId)) {
            //获取专题名称
            topicName = topicDOMapper.selectNameByPrimaryKey(topicId);
            //判断一下提交的话题,是否事真实存在的
            if (StringUtils.isBlank(topicName)) throw new BizException(ResponseCodeEnum.TOPIC_NOT_FOUND);
        }
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);
        //更新笔记元数据表 t_note
        String content=updateNoteReqVO.getContent();
        NoteDO noteDO=NoteDO.builder()
                .id(noteId)
                .isContentEmpty(StringUtils.isBlank(content))
                .imgUris(imgUris)
                .title(updateNoteReqVO.getTitle())
                .topicId(updateNoteReqVO.getTopicId())
                .topicName(topicName)
                .type(type)
                .updateTime(LocalDateTime.now())
                .videoUri(videoUri)
                .build();
        noteDOMapper.updateByPrimaryKey(noteDO);
        //删除 Redis 缓存MQ:删除笔记本地缓存发送成功
//一致性保证:延迟双删策略
        //异步发送延时消息
       Message<String> message= MessageBuilder.withPayload(String.valueOf(noteId)).build();
        rocketMQTemplate.asyncSend(MQConstants.TOPIC_DELAY_DELETE_NOTE_REDIS_CACHE, message,
                new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        log.info("===>MQ:延时删除Redis笔记缓存 发送成功...");
                    }

                    @Override
                    public void onException(Throwable throwable) {
                        log.error("===>MQ:延迟删除 Redis 笔记缓存 发送失败...", throwable);
                    }
                }, 3000,//超时时间(毫秒)
                1 //延迟级别,1表示延时1s
        );

        //    redisTemplate.delete(noteDetailRedisKey);
      //  //删除本地缓存
      //  LOCAL_CACHE.invalidate(noteId);
        //同步发送广播模式MQ,将所有实例中的本地缓存都删掉
        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("===>MQ:删除笔记本地缓存发送成功...");

        //笔记内容更新
        //查询此片笔记内容对应UUID
        NoteDO noteDo1=noteDOMapper.selectByPrimaryKey(noteId);
        String contentUuid = noteDo1.getContentUuid();

        //笔记内容是否更新成功
        boolean isUpdateContentSuccess=false;
        if (StringUtils.isBlank(contentUuid)){
            //若笔记内容为null Ze 删除K-V存储
            isUpdateContentSuccess=keyValueRpcService.deleteNoteContent(contentUuid);
        }else{
            //若将无内容的笔记,更新为有内容的笔记,需要重新生成UUID
            contentUuid=StringUtils.isBlank(contentUuid)? UUID.randomUUID().toString() : contentUuid;
            //调用哦那个K-V服务更新短文本
            isUpdateContentSuccess=keyValueRpcService.saveNoteContent(contentUuid, content);
        }
        //如果更新失败,爬出业务异常,回滚事务
        if (!isUpdateContentSuccess){
            throw new BizException(ResponseCodeEnum.NOTE_UPDATE_FAIL);
        }
        return Response.success();

    }

    /**
     * 删除笔记缓存
     *
     * @param noteId
     */
    @Override
    public void deleteNoteLocalCache(Long noteId) {
        LOCAL_CACHE.invalidate(noteId);
    }

    /**
     * @param deleteNoteReqVO
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Response<?> deleteNote(DeleteNoteReqVO deleteNoteReqVO) {
        // 笔记 ID
        Long noteId = deleteNoteReqVO.getId();
        NoteDO selectNoteDO=noteDOMapper.selectByPrimaryKey(noteId);
        //判断笔记是否存在
        if (Objects.isNull(selectNoteDO)){
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }
        //判断权限:非笔记发布者不允许删除笔记
        Long currUserId=LoginUserContextHolder.getUserId();
        if (!Objects.equals(currUserId,selectNoteDO.getCreatorId())){
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }

        //逻辑删除
        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .status(NoteStatusEnum.DELETED.getCode())
                .updateTime(LocalDateTime.now())
                .build();

        int count = noteDOMapper.updateByPrimaryKeySelective(noteDO);

        //若影响的行为为 0 则表示该笔记不存在
        if (count == 0) {
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }
        //删除缓存
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);

        //同步发送广播模式MQ,将所有实例中的本地缓存都删掉(原因是本地缓存会存在于服务中的)
        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("===>MQ:删除笔记本地缓存发送成功...");

        //发送 MQ
        //构建消息体DTO
        NoteOperateMqDTO noteOperateMqDTO = NoteOperateMqDTO.builder()
                .creatorId(selectNoteDO.getCreatorId())
                .noteId(noteId)
                .type(NoteOperateEnum.DELETE.getCode())//删除笔记
                .build();

        //构建消息对象,并将DTO 转成Json字符串设置到消息体中
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(noteOperateMqDTO))
                .build();
        //通过冒号连接,可让MQ发送给主题Topic时,携带上标签 Tag
        String destination = MQConstants.TOPIC_NOTE_OPERATE + ":" + MQConstants.TAG_NOTE_DELETE;
        //异步发送MQ 消息,提升接口响应速度
        rocketMQTemplate.asyncSend(destination, message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 【笔记删除】MQ 发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 【笔记删除】MQ 发送异常: ", throwable);
            }
        });

        return Response.success();
    }

    /**
     * 笔记仅自己可见
     * @param updateNoteVisibleOnlyMeReqVO
     * @return
     */
    @Override
    public Response<?> visibleOnlyMe(UpdateNoteVisibleOnlyMeReqVO updateNoteVisibleOnlyMeReqVO) {
        //笔记ID
        Long noteId = updateNoteVisibleOnlyMeReqVO.getId();

        NoteDO selectNoteDO=noteDOMapper.selectByPrimaryKey(noteId);
        //判断笔记是否存在
        if (Objects.isNull(selectNoteDO)){
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }
        //判断权限:非笔记发布者不允许修改笔记权限
        Long currUserId=LoginUserContextHolder.getUserId();
        if (!Objects.equals(currUserId,selectNoteDO.getCreatorId())){
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }


        //构建更新DO实体类
        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .visible(NoteVisibleEnum.PRIVATE.getCode())
                .updateTime(LocalDateTime.now())
                .build();
        //执行更新 SQL
        int count = noteDOMapper.updateVisibleOnlyMe(noteDO);

        //若影响的行为数为 0 ,则表示该笔记无法修改为仅自己可见
        if (count == 0){
            throw new BizException(ResponseCodeEnum.NOTE_CANT_VISIBLE_ONLY_ME);
        }
        //删除Redis 缓存
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);

        //同步发送广播模式,MQ ,将所有实例中的本地缓存都删除掉
        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("===>MQ:删除笔记本地缓存发送成功...");

        return Response.success();
    }

    /**
     * 笔记置顶/ 取消置顶
     *
     * @param topNoteReqVO
     * @return
     */
    @Override
    public Response<?> topNote(TopNoteReqVO topNoteReqVO) {
        // 笔记 ID
        Long noteId = topNoteReqVO.getId();
        //是否置顶
        Boolean isTop = topNoteReqVO.getIsTop();

        //当前登录用户ID
        Long currUserId = LoginUserContextHolder.getUserId();

        //构建置顶/取消置顶 DO 实体 类
        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .isTop(isTop)
                .updateTime(LocalDateTime.now())
                .creatorId(currUserId)//只有笔记所有者,才能置顶/取消置顶
                .build();
        int count = noteDOMapper.updateIsTopInt(noteDO);

        if (count == 0){
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }

        //删除redis 缓存
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);

        //同步发送广播模式MQ,将所有实例中的本地和缓存都删除
        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("===>MQ:删除笔记本地缓存发送成功...");

        return Response.success();
    }

    /**
     * 点赞笔记
     *
     * @param likeNoteReqVo
     * @return
     */
    @Override
    public Response<?> likeNote(LikeNoteReqVo likeNoteReqVo) {
        Long noteId = likeNoteReqVo.getId();
        //1 校验被点赞的笔记是否存在,若存在,则获取发布者用户 ID
        Long creatorId = checkNoteIsExistAndGetCreatorId(noteId);


        //2 判断目标笔记,是否已经被点赞过
        //当前登录用户 ID
        Long userId = LoginUserContextHolder.getUserId();
        //布隆过滤器 Key
        String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId);

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        //Lua 脚本路径
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_like_check.lua")));
        
        //返回值类型
        script.setResultType(Long.class);
        
        //执行Lua 脚本 ,拿到返回结果
        Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId);
        NoteLikeLuaResultEnum noteLikeLuaResultEnum = NoteLikeLuaResultEnum.valueOf(result);

        //用户点赞列表 ZSet Key
        String userNoteLikeZSetKey =RedisKeyConstants.buildUserNoteLikeZSetKey(userId);
        switch (noteLikeLuaResultEnum){
            //Redis 中 布隆过滤器不存在
            case NOT_EXIST -> {
                // 从数据库中校验笔记是否被点赞,并异步初始化布隆过滤器,设置过期时间
                int count = noteDOMapper.selectCountByUserIdAndNoteId(userId, noteId);

                //保底1天+随机秒数
                long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);

                //目标 笔记已经被点赞
                if (count > 0) {
                    //异步初始化布隆过滤器
                 //   asynBatchAddNoteLike2BloomAndExpire(userId,expireSeconds,bloomUserNoteLikeListKey);
                    //异步初始化布隆过滤器,将该用户点赞的其他用户笔记也初始化到布隆过滤器中
                    batchAddNoteLike2BloomAndExpire(userId,expireSeconds,bloomUserNoteLikeListKey);

                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
                }
                //若数据库中也没有点赞记录,说明该用户还未点赞过任何笔记
                batchAddNoteLike2BloomAndExpire(userId,expireSeconds,bloomUserNoteLikeListKey);
                //Llua脚本路径
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/Lua/bloom_add_note_like_and_expire.lua")));
                //返回值类型
                script.setResultType(Long.class);
                redisTemplate.execute(script,Collections.singletonList(bloomUserNoteLikeListKey),noteId,expireSeconds);
            }
            // 目标笔记已经把被点赞
            case NOTE_LIKED ->{
                //校验 ZSet 列表中是否包含被点赞的笔记ID
                Double score = redisTemplate.opsForZSet().score(userNoteLikeZSetKey, noteId);
                if (Objects.nonNull(score)){
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
                }
                //若 Score 为空,则表示ZSet 点赞列表中不存在,查询数据库校验
                int count = noteLikeDOMapper.selectNoteIsLiked(userId, noteId);
                if(count>0){
                    // 数据库里面有点赞记录,而Redis 中ZSet不存在 需要重新异步初始化ZSet
                    asynInitUserNoteLikesZSet(userId,userNoteLikeZSetKey);
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
                }
            }
        }

        // 3 更新用户 ZSET 点赞列表
        LocalDateTime now = LocalDateTime.now();
        //Lua 脚本路径
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/note_like_check_and_upate_zset.lua")));
        //返回值类型
        script.setResultType(Long.class);
        //执行Lua 脚本,拿到返回结果
        result=redisTemplate.execute(script,Collections.singletonList(userNoteLikeZSetKey),noteId, DateUtils.localDateTime2Timestamp(now));

        //若 ZSet 列表不存在,需要重新初始化
        if (Objects.equals(result,NoteLikeLuaResultEnum.NOT_EXIST.getCode())){
            // 查询当前用户最新点赞的100 篇笔记
            List<NoteLikeDO> noteLikeDOS = noteLikeDOMapper.selectLikedByUserIdAndLimit(userId, 100);

            //保底1天+随机秒数
            long expireSeconds=60*60*24 +RandomUtil.randomInt(60*60*24);
            DefaultRedisScript<Long> script2= new DefaultRedisScript<Long>();
            //Lua 脚本路径
            script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_like_zset_and_expire.lua")));
            //返回值类型
            script2.setResultType(Long.class);
            //若数据库中存在点赞记录,需要批量同步
            if (CollUtil.isNotEmpty(noteLikeDOS)){
                //构建Lua 参数
               Object[]  luaArgs=buildNoteLikeZSetLuaArgs(noteLikeDOS,expireSeconds);
               redisTemplate.execute(script2,Collections.singletonList(userNoteLikeZSetKey),luaArgs);
               //再次调用 note_like_check_and_update_zset.lua 脚本,将点赞的笔记添加到zset中
               redisTemplate.execute(script,Collections.singletonList(userNoteLikeZSetKey),noteId,DateUtils.localDateTime2Timestamp(now));

            }else{ //若数据库中,无点赞过的笔记记录,则直接将当前点赞的笔记ID 添加到ZSet中,随机过期时间
                ArrayList<Object> luaArgs = Lists.newArrayList();
                luaArgs.add(DateUtils.localDateTime2Timestamp(LocalDateTime.now()));//score :点赞时间戳
                luaArgs.add(noteId);//当前点赞的笔记ID
                luaArgs.add(expireSeconds);//随机过期时间

                redisTemplate.execute(script2,Collections.singletonList(userNoteLikeZSetKey),luaArgs.toArray());

            }
        }
        // 4发送 MQ,将点赞数据落库
        // 构建消息体DTO
        LikeUnlikeNoteMqDTO likeUnlikeNoteMqDTO = LikeUnlikeNoteMqDTO.builder()
                .userId(userId)
                .noteId(noteId)
                .type(LikeUnlikeNoteTypeEnum.LIKE.getCode())
                .createTime(now)
                .noteCreatorId(creatorId)// 笔记发布者 ID
                .build();
        //构建消息对象,并将DTO 转成 Json 字符串设置到消息体中
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(likeUnlikeNoteMqDTO)).build();
        //通过冒号连接,可让MQ 发送主题Topic 时,携带上标签Tag
        String destination = MQConstants.TOPIC_LIKE_OR_UNLIKE + ":" + MQConstants.TAG_LIKE;
        String hashKey = String.valueOf(userId);
        //异步发送MQ消息,提升接口响应速度
        rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("===> [笔记点赞] MQ 发送成功,SendResult:{}",sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("===> [笔记点赞] MQ 发送异常:",throwable);

            }
        });


        return null;
    }

    /**
     * 取消点赞笔记
     *
     * @param unlikeNoteReqVO
     * @return
     */
    @Override
    public Response<?> unlikeNote(UnlikeNoteReqVO unlikeNoteReqVO) {
        //笔记 ID
        Long noteId = unlikeNoteReqVO.getId();

        //1校验笔记是否真实存在
        Long creatorId = checkNoteIsExistAndGetCreatorId(noteId);
        // 2 校验笔记是否被点赞过
        //当前登录用户 ID


        Long userId = LoginUserContextHolder.getUserId();
        //布隆过滤器Key
        String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId);

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        //Lua 脚本路径
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_unlike_check.lua")));
        //返回值类型
        script.setResultType(Long.class);

        //执行 Lua 脚本,拿到返回结果
        Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId);
        NoteUnlikeLuaResultEnum noteUnlikeLuaResultEnum = NoteUnlikeLuaResultEnum.valueOf(result);

        switch (noteUnlikeLuaResultEnum){
            //布隆过滤器不存在
            case NOT_EXIST -> {
                // 异步初始化布隆过滤器
                threadPoolTaskExecutor.submit(()->{
                    //保底 1天+ 随机秒数
                    int expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
                    batchAddNoteLike2BloomAndExpire(userId,expireSeconds,bloomUserNoteLikeListKey);
                });
                //从数据库中校验笔记是否被点赞
                int count = noteLikeDOMapper.selectNoteIsLiked(userId, noteId);

                //未 点赞 ,无法取消点赞操作,抛出业务异常
                if (count==0) throw new BizException(ResponseCodeEnum.NOTE_NOT_LIKED);
            }
            //布隆过滤器校验目标笔记 未被点赞(判断 绝对正确)
            case NOTE_NOT_LIKED -> throw new BizException(ResponseCodeEnum.NOTE_NOT_LIKED);


        }
        // 3 删除ZSET中已点赞的笔记ID
        //能走到这里,说明布隆过滤器判断已点赞,直接删除 ZSET 中已点赞的笔记ID
        //用户点赞列表ZSet Key
        String NoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);

        redisTemplate.opsForZSet().remove(NoteLikeZSetKey,noteId);

        // 4 发送 MQ 数据更新落库'
        //构建消息体 DTO
        LikeUnlikeNoteMqDTO likeUnlikeNoteMqDTO = LikeUnlikeNoteMqDTO.builder()
                .userId(userId)
                .noteId(noteId)
                .type(LikeUnlikeNoteTypeEnum.UNLIKE.getCode())//取消点赞笔记
                .createTime(LocalDateTime.now())
                .noteCreatorId(creatorId) //笔记发布者ID
                .build();

        //构建消息对象,并将DTO转成Json字符串设置到消息体中

        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(likeUnlikeNoteMqDTO)).build();
        //通过冒号连接,可让MQ 发送给主题Topic 时,携带上标签Tag
        String destination = MQConstants.TOPIC_LIKE_OR_UNLIKE + ":" + MQConstants.TAG_UNLIKE;
        String hashKey=String.valueOf(userId);

        //异步发送MQ 消息提升接口响应速度
        rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("===> [笔记取消点赞] MQ 发送成功,SendResult:{}",sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.info("===> [笔记取消点赞]MQ 发送异常",throwable);
            }
        });

        return Response.success();
    }

    /**
     * 收藏笔记
     *
     * @param collectNoteReqVO
     * @return
     */
    @Override
    public Response<?> collectNote(CollectNoteReqVO collectNoteReqVO) {
        //笔记 ID
        Long noteId = collectNoteReqVO.getId();
        //1.校验被收藏的笔记是否存在
        checkNoteIsExistAndGetCreatorId(noteId);

        //TODO: 2判断目标笔记,是否已经收藏过
        // 当前登录用户ID
        Long userId = LoginUserContextHolder.getUserId();
        //布隆过滤器Key
        String bloomUSerNoteCollectListKey = RedisKeyConstants.buildBloomUSerNoteCollectListKey(userId);

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        //Lua 脚本路径
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_collect_check.lua")));
        //返回值类型
        script.setResultType(Long.class);
        
        //执行 Lua脚本 ,拿到返回结果
        Long  result=redisTemplate.execute(script, Collections.singletonList(bloomUSerNoteCollectListKey), noteId);

        NoteCollectLuaResultEnum noteCollectLuaResultEnum=NoteCollectLuaResultEnum.valueOf(result);
        String userNoteCollectZSetKey = RedisKeyConstants.buildUserNoteCollectZSetKey(userId);
        switch (noteCollectLuaResultEnum){
            //Redis 中布隆过滤器不存在
            case NOT_EXIST -> {
                // 从数据库中校验笔记是否被收藏,并异步初始化布隆过滤器,设置过期时间
             int count=noteCollectionDOMapper.selectCountByUserIdAndNoteId(userId,noteId);

             //保底 1天+随机秒数
             long expireSeconds=60*60*24+RandomUtil.randomInt(60*60*24);
             //目标笔记已经被收藏
                if (count>0){
                    //TODO 异步初始化布隆过滤器
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_COLLECTED);
                }
                //x 若目标笔记未被收藏,查询当前用户是否有收藏其他笔记,有则同步初始化布隆
                batchAddNoteCollect2BloomAndExpire(userId,expireSeconds,bloomUSerNoteCollectListKey);

                //添加当前收藏笔记,ID到布隆过滤器中
                //Lua脚本路径
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_add_note_collect_and_expire.lua")));
                //返回值类型
                script.setResultType(Long.class);
                redisTemplate.execute(script,Collections.singletonList(bloomUSerNoteCollectListKey),noteId,expireSeconds);
            }
            //构建 REdis key
            // 目标 笔记已经被收藏(可能存在误判,需要进一步确定)
            case NOTE_COLLECTED -> {
                // 校验 ZSet 列表中 是否包含被收藏的笔记ID
                Double score = redisTemplate.opsForZSet().score(userNoteCollectZSetKey, noteId);

                if (Objects.nonNull(score)){
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_COLLECTED);
                }
                //若 Score为空,则表示ZSet 收藏列表中不存在,查询数据库校验
                int count = noteCollectionDOMapper.selectNoteIsCollected(userId, noteId);
                if (count>0){
                    //数据库里面有收藏记录,而Redis中Zset 已经被删除的花,需要重新异步初始化Zset
                    asynInitUserNoteCollectionsZSet(userId,userNoteCollectZSetKey);
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_COLLECTED);

                }
            }
        }
        // 3 更新用户ZSET 收藏列表
        LocalDateTime now = LocalDateTime.now();
        //Lua 脚本类型
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/note_collect_check_and_update_zset.lua")));
        //返回值类型
        script.setResultType(Long.class);

        //执行 Lua 脚本,拿到返回结果
        result=redisTemplate.execute(script,Collections.singletonList(userNoteCollectZSetKey),noteId,DateUtils.localDateTime2Timestamp(now));
        //若 ZSet列表不存在,需要重新初始化
        if (Objects.equals(result,NoteCollectLuaResultEnum.NOT_EXIST.getCode())){
            // 查询 当前用户最新收藏的 300 篇笔记
            List<NoteCollectionDO> noteCollectionDOS = noteCollectionDOMapper.selectCollectedByUserIdAndLimit(userId, 300L);

            //保底1天 +随机秒数
            long expireSseconds=60*60*24+RandomUtil.randomInt(60*60*24);
            DefaultRedisScript<Long> script2=new DefaultRedisScript<Long>();
            //Lua 脚本路径
            script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_collect_zset_and_expire.lua")));
            //返回值类型
            script2.setResultType(Long.class);
            //若数据库中在历史收藏笔记,需要批量同步
            if (CollUtil.isNotEmpty(noteCollectionDOS)){
                //构建 Lua参数
                Object[] luaArgs = buildNoteCollectZSetLuaArgs(noteCollectionDOS, expireSseconds);
                redisTemplate.execute(script2,Collections.singletonList(userNoteCollectZSetKey),luaArgs);
                //再次调用 note_collect_check_and_update_zset.lua脚本,将当前收藏的笔记 添加到zset 中
                redisTemplate.execute(script,Collections.singletonList(userNoteCollectZSetKey),noteId,DateUtils.localDateTime2Timestamp(now));
            }else{//若无历史 收藏的笔记,则直接将当前收藏的笔记 ID 添加到ZSet 中 随机过期时间
                ArrayList<Object> luaArgs = Lists.newArrayList();
                luaArgs.add(DateUtils.localDateTime2Timestamp(LocalDateTime.now()));//score:收藏时间戳
                luaArgs.add(noteId); //当前收藏的笔记ID
                luaArgs.add(expireSseconds);//随机过期时间

                redisTemplate.execute(script2,Collections.singletonList(userNoteCollectZSetKey),luaArgs.toArray());

            }
        }

        // 4 发送MQ 将收藏数据落库
        //构建消息体DTO
        CollectUnCollectNoteMqDTO collectUnCollectNoteMqDTO = CollectUnCollectNoteMqDTO.builder()
                .userId(userId)
                .noteId(noteId)
                .type(CollectUnCollectNoteTypeEnum.COLLECT.getCode())//收藏笔记
                .createTime(now)
                .build();

        //构建消息对象,并将DTO转成 JSon 字符串设置到消息体中
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(collectUnCollectNoteMqDTO))
                .build();
        // 通过冒号连接,可让MQ 发送给主题 Topic时,携带上标签Tag
        String destination = MQConstants.TOPIC_COLLECT_OR_UN_COLLECT + ":" + MQConstants.TAG_COLLECT;

        String hashKey = String.valueOf(userId);
        //异步发送顺序MQ 消息,提升接口响应速度
        rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==>[笔记收藏] MQ发送成功,SendResult:{}",sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> [笔记收藏] MQ发送异常:",throwable);
            }
        });

        return Response.success();
    }

    /**
     * Redis中ZSet 未初始化,需要重新异步初始化 ZSet
     * @param userId
     * @param userNoteCollectionZSetKey
     */
    private void asynInitUserNoteCollectionsZSet(Long userId,String userNoteCollectionZSetKey){
        threadPoolTaskExecutor.execute(()->{
            //判断 用户笔记收藏 ZSET 是否存在
            Boolean hasKey = redisTemplate.hasKey(userNoteCollectionZSetKey);
            //不存在,则重新初始化
            if (!hasKey){
                //查询当前用户最新收藏的300 篇笔记
                List<NoteCollectionDO> noteCollectionDOS = noteCollectionDOMapper.selectCollectedByUserIdAndLimit(userId, 300L);
                if (CollUtil.isNotEmpty(noteCollectionDOS)){
                    //保底 1天+随机秒数
                    long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 25);
                    //构建 Lua 参数
                    Object[] luaArgs=buildNoteCollectZSetLuaArgs(noteCollectionDOS,expireSeconds);
                    DefaultRedisScript<Long> script2 = new DefaultRedisScript<>();
                    //Lua 脚本路径
                    script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_collect_zset_and_expire.lua")));
                    //返回值类型
                    script2.setResultType(Long.class);
                    redisTemplate.execute(script2,Collections.singletonList(userNoteCollectionZSetKey),luaArgs,expireSeconds);
                }

            }

        });
    }

    /**
     * 构建笔记收藏 ZSET Lua 脚本参数
     * @param noteCollectionDOS
     * @param expireSeconds
     * @return
     */
    private Object[] buildNoteCollectZSetLuaArgs(List<NoteCollectionDO> noteCollectionDOS, long expireSeconds) {
        int argsLength=noteCollectionDOS.size()*2+1;
        Object[] luaArgs=new Object[argsLength];

        int i=0;
        for (NoteCollectionDO noteCollectionDO:noteCollectionDOS){
            luaArgs[i]=DateUtils.localDateTime2Timestamp(noteCollectionDO.getCreateTime());
            luaArgs[i+1]=noteCollectionDO.getNoteId(); //笔记ID 作为 Zset value
            i+=2;
        }
        luaArgs[argsLength-1]=expireSeconds;//最后一个参数是ZSet 的过期时间
        return luaArgs;
    }

    private void batchAddNoteCollect2BloomAndExpire(Long userId,long expireSeconds,String bloomUserNoteCollectListKey){
        try{
            //异步全量同步一下,并设置过期时间
            List<NoteCollectionDO> noteCollectionDOS = noteCollectionDOMapper.selectByUserId(userId);
            if(CollUtil.isNotEmpty(noteCollectionDOS)){
                DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                //Lua脚本路径
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_batch_add_note_collect_and_expire.lua")));
                //返回值类型
                script.setResultType(Long.class);
                //构建Lua参数
                ArrayList<Object> luaArgs = Lists.newArrayList();
                noteCollectionDOS.forEach(noteCollectionDO -> luaArgs.add(noteCollectionDO.getNoteId()));//将每一个收藏的笔记ID传入
                luaArgs.add(expireSeconds);//最后一个参数是过期时间
                redisTemplate.execute(script,Collections.singletonList(bloomUserNoteCollectListKey),luaArgs.toArray());
            }

        }catch (Exception e){
            log.error("## 异步初始化[笔记收藏] 布隆过滤器异常:",e);
        }
    }

    /**
     * 异步初始化布隆过滤器
     * @param userId
     * @param expireSeconds
     * @param bloomUserNoteLikeListKey
     */
    private void batchAddNoteLike2BloomAndExpire(Long userId, long expireSeconds, String bloomUserNoteLikeListKey) {
        try{
            //异步全量同步一下,并设置过期时间
            List<NoteLikeDO> noteLikeDOS = noteLikeDOMapper.selectByUserId(userId);
            if (CollUtil.isNotEmpty(noteLikeDOS)){
                DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                //Lua脚本
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_batch_add_note_like_and_expire.lua")));
                //返回值类型
                script.setResultType(Long.class);
                //构建 Lua参数
                ArrayList<Object> luaArgs = Lists.newArrayList();
                noteLikeDOS.forEach(noteLikeDO -> luaArgs.add(noteLikeDO.getNoteId()));//将每个点赞的笔记ID传入
                luaArgs.add(expireSeconds);//最后一个参数时过期时间(秒)
                redisTemplate.execute(script,Collections.singletonList(bloomUserNoteLikeListKey),luaArgs.toArray());
            }
        }catch (Exception e){
            log.error("## 异步初始化布隆过滤器异常:",e);
        }

    }

    /**
     * 异步初始化用户点赞笔记ZSet
     * @param userId
     * @param userNoteLikeZSetKey
     */
    private void asynInitUserNoteLikesZSet(Long userId, String userNoteLikeZSetKey) {
        threadPoolTaskExecutor.execute(()->{
            //判断用户笔记点赞 ZSET是否存在
            Boolean hasKey = redisTemplate.hasKey(userNoteLikeZSetKey);
            //不存在,则重新初始化
            if (!hasKey){
                //查询当前用户最新点赞的100篇笔记
                List<NoteLikeDO> noteLikeDOS = noteLikeDOMapper.selectLikedByUserIdAndLimit(userId, 100);
                if (CollUtil.isNotEmpty(noteLikeDOS)){
                    //保底1天+随机秒数
                    long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);

                    //构建 Lua 参数
                    Object[] luaArgs = buildNoteLikeZSetLuaArgs(noteLikeDOS, expireSeconds);
                    DefaultRedisScript<Long> script2 = new DefaultRedisScript<>();
                    //Lua 脚本路径
                    script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua//batch_add_note_like_zset_and_expire.lua")));
                    //返回值类型
                    script2.setResultType(Long.class);
                    redisTemplate.execute(script2,Collections.singletonList(userNoteLikeZSetKey),luaArgs);
                }

            }
        });

    }

    private Object[] buildNoteLikeZSetLuaArgs(List<NoteLikeDO> noteLikeDOS, long expireSeconds) {
        int argsLength=noteLikeDOS.size()*2+1;// 每个笔记点赞关系有2个参数(score 和value),最后再跟一个过期时间
        Object[] luaArgs= new Object[argsLength];

        int i=0;
        for (NoteLikeDO noteLikeDO:noteLikeDOS){
           luaArgs[i] = DateUtils.localDateTime2Timestamp(noteLikeDO.getCreateTime());
           luaArgs[i+1]=noteLikeDO.getNoteId();
           i+=2;
        }
        luaArgs[argsLength-1]=expireSeconds;//最后一个参数是ZSet的过期时间
        return luaArgs;

    }

    /**
     * 异步初始化布隆过滤器
     * @param userId
     * @param expireSeconds
     * @param bloomUserNoteLikeListKey
     */
    private void asynBatchAddNoteLike2BloomAndExpire(Long userId,long expireSeconds, String bloomUserNoteLikeListKey) {
        threadPoolTaskExecutor.submit(()->{
            try{
                //异步全量同步一下,并设置过期时间
                List<NoteLikeDO> noteLikeDOS = noteLikeDOMapper.selectByUserId(userId);
                if (CollUtil.isNotEmpty(noteLikeDOS)){
                    DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                    //Lua 脚本路径
                    script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_batch_add_note_like_and_expire.lua")));
                    //返回值类型
                    script.setResultType(Long.class);

                    //构建Lua 参数
                    List< Object> luaArgs = Lists.newArrayList();
                    noteLikeDOS.forEach(noteLikeDO -> luaArgs.add(noteLikeDO.getNoteId()));
                    luaArgs.add(expireSeconds);
                    redisTemplate.execute(script,Collections.singletonList(bloomUserNoteLikeListKey),luaArgs.toArray());
                }

            }catch (Exception e){
                log.error("## 异步初始化布隆过滤器异常:",e);
            }
        });

    }

    /**
     * 校验笔记是否存在,若存在,则获取笔记的发布者ID
     * @param noteId
     */
    private Long checkNoteIsExistAndGetCreatorId(Long noteId) {
        //先从本地缓存校验
        String findNoteDetailRspVOStrLocalCache = LOCAL_CACHE.getIfPresent(noteId);
        //解析Json 字符串为VO对象
        FindNoteDetailRspVO findNoteDetailRspVO = JsonUtils.parseObject(findNoteDetailRspVOStrLocalCache, FindNoteDetailRspVO.class);
        // 若本地缓存没有
        if (Objects.isNull(findNoteDetailRspVO)){
            //再从redis 中校验
            String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
            String noteDetailJson = redisTemplate.opsForValue().get(noteDetailRedisKey);

            //解析Json 字符串为VO 对象
          findNoteDetailRspVO=  JsonUtils.parseObject(noteDetailJson,FindNoteDetailRspVO.class);
            //都不存在,再查询数据库校验是否存在
            if (Objects.isNull(findNoteDetailRspVO)){
                //笔记发布者用户 ID
                Long creatorId = noteDOMapper.selectCreateIdByNoteId(noteId);

                //若数据库中也不存在,提示用户
                if (Objects.isNull(creatorId)){
                    throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
                }

         /*       int count=noteDOMapper.selectCountByNoteId(noteId);

                //若数据库中也不存在,提示用户
                if (count ==0){
                    throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
                }*/
                //若数据库中存在,异步同步一下缓存
                threadPoolTaskExecutor.submit(()->{
                    FindNoteDetailReqVo findNoteDetailReqVo = FindNoteDetailReqVo.builder().id(noteId).build();
                    findNoteDetail(findNoteDetailReqVo);
                });
                return creatorId;
            }
        }
        return findNoteDetailRspVO.getCreatorId();
    }


    /**
     * 取消收藏笔记
     *
     * @param unCollectNoteReqVO
     * @return
     */
    @Override
    public Response<?> unCollectNote(UnCollectNoteReqVO unCollectNoteReqVO) {
        // 笔记ID
        long noteId = unCollectNoteReqVO.getId();
        //1校验笔记是否真实存在
        checkNoteIsExistAndGetCreatorId(noteId);

        // 2 校验笔记是否被收藏过
        //当前 登录用户ID
        Long userId = LoginUserContextHolder.getUserId();

        //布隆过滤器Key
        String bloomUSerNoteCollectListKey = RedisKeyConstants.buildBloomUSerNoteCollectListKey(userId);

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        //Lua 脚本路径
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_uncollect_check.lua")));
        //返回值类型
        script.setResultType(Long.class);
        //执行Lua脚本,拿到返回结果
        Long result = redisTemplate.execute(script, Collections.singletonList(bloomUSerNoteCollectListKey), noteId);

        NoteCollectLuaResultEnum noteCollectLuaResultEnum = NoteCollectLuaResultEnum.valueOf(result);

        switch (noteCollectLuaResultEnum){
            //布隆过滤器不存在
            case NOT_EXIST -> {
                // 异步初始化布隆过滤器
                threadPoolTaskExecutor.submit(()->{
                    long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
                    batchAddNoteCollect2BloomAndExpire(userId,expireSeconds,bloomUSerNoteCollectListKey);
                });
                //从数据库中校验笔记是否被收藏
                int count = noteCollectionDOMapper.selectCountByUserIdAndNoteId(userId, noteId);

                //未收藏,无法取消收藏操作,抛出业务异常
                if (count == 0) throw new BizException(ResponseCodeEnum.NOTE_NOT_COLLECTED);

            }
            //布隆过滤器校验目标笔记未被收藏(判断绝对正确)
            case NOTE_NOT_COLLECTED -> throw new BizException(ResponseCodeEnum.NOTE_NOT_COLLECTED);
        }

        // 3 删除 ZSET中已收藏的笔记ID
        //能走到这里,说明布隆过滤器判断已收藏,现在直接删除ZSET中,已收藏的笔记ID
        //用户收哦从列表ZSet Key
        String uSerNoteCollectListKey=RedisKeyConstants.buildUserNoteCollectZSetKey(userId);

        redisTemplate.opsForZSet().remove(uSerNoteCollectListKey,noteId);
        //TODO 4 发送MQ 数据更新落库
        //构建消息体 DTO
        //构建消息对象,并将DTO转成Json字符串设置到消息体中

        CollectUnCollectNoteMqDTO unCollectNoteMqDTO = CollectUnCollectNoteMqDTO.builder()
                .userId(userId)
                .noteId(noteId)
                .type(CollectUnCollectNoteTypeEnum.UN_COLLECT.getCode())
                .createTime(LocalDateTime.now())
                .build();
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(unCollectNoteMqDTO))
                .build();
        //构建消息对象,并将DTO 转成 Json字符串设置到消息体中
        String destination = MQConstants.TOPIC_COLLECT_OR_UN_COLLECT + ":" + MQConstants.TAG_UN_COLLECT;
        String hashKey=String.valueOf(userId);
        //异步 发送顺序 MQ 消息,提升接口响应速度
        rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 【笔记取消收藏】MQ 发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 【笔记取消收藏】MQ 发送异常: ", throwable);
            }
        });

        return Response.success();
    }


}
